/**
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.apache.isis.viewer.restfulobjects.rendering.domainobjects;

import java.util.List;

import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller;
import org.apache.isis.core.metamodel.consent.Consent;
import org.apache.isis.core.metamodel.facets.object.notpersistable.NotPersistableFacet;
import org.apache.isis.core.metamodel.facets.object.title.TitleFacet;
import org.apache.isis.core.metamodel.services.ServiceUtil;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.spec.feature.Contributed;
import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
import org.apache.isis.core.runtime.system.context.IsisContext;
import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
import org.apache.isis.viewer.restfulobjects.applib.Rel;
import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
import org.apache.isis.viewer.restfulobjects.applib.RestfulHttpMethod;
import org.apache.isis.viewer.restfulobjects.rendering.LinkBuilder;
import org.apache.isis.viewer.restfulobjects.rendering.LinkFollowSpecs;
import org.apache.isis.viewer.restfulobjects.rendering.RendererContext;
import org.apache.isis.viewer.restfulobjects.rendering.ReprRendererAbstract;
import org.apache.isis.viewer.restfulobjects.rendering.domaintypes.DomainTypeReprRenderer;
import org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils;

public class DomainObjectReprRenderer extends ReprRendererAbstract<DomainObjectReprRenderer, ObjectAdapter> {

    private static final String X_RO_DOMAIN_TYPE = "x-ro-domain-type";

    public static LinkBuilder newLinkToBuilder(final RendererContext rendererContext, final Rel rel, final ObjectAdapter objectAdapter) {
        String domainType = OidUtils.getDomainType(objectAdapter);
        String instanceId = OidUtils.getInstanceId(rendererContext, objectAdapter);
        final String url = "objects/" + domainType + "/" + instanceId;
        return LinkBuilder.newBuilder(rendererContext, rel.getName(), RepresentationType.DOMAIN_OBJECT, url).withTitle(objectAdapter.titleString(null));
    }

    private static enum Mode {
        REGULAR, 
        PERSIST_LINK_ARGUMENTS, 
        UPDATE_PROPERTIES_LINK_ARGUMENTS,
        EVENT_SERIALIZATION;

        public boolean isRegular() {
            return this == REGULAR;
        }

        public boolean isPersistLinkArgs() {
            return this == PERSIST_LINK_ARGUMENTS;
        }
        
        public boolean isUpdatePropertiesLinkArgs() {
            return this == UPDATE_PROPERTIES_LINK_ARGUMENTS;
        }

        public boolean isEventSerialization() {
            return this == EVENT_SERIALIZATION;
        }

        public boolean includeDescribedBy() {
            return isRegular() || isPersistLinkArgs();
        }

        public boolean includeUp() {
            return isRegular();
        }

        public boolean checkVisibility() {
            return isRegular() || isUpdatePropertiesLinkArgs();
        }

        public boolean isArgs() {
            return isPersistLinkArgs() || isUpdatePropertiesLinkArgs();
        }
    }

    private ObjectAdapterLinkTo linkToBuilder;
    private ObjectAdapter objectAdapter;
    private Mode mode = Mode.REGULAR;

    public DomainObjectReprRenderer(final RendererContext resourceContext, final LinkFollowSpecs linkFollower, final JsonRepresentation representation) {
        super(resourceContext, linkFollower, RepresentationType.DOMAIN_OBJECT, representation);
        usingLinkToBuilder(new DomainObjectLinkTo());
    }

    /**
     * Override the default {@link ObjectAdapterLinkTo} (that is used for
     * generating links in {@link #linkTo(ObjectAdapter)}).
     */
    public DomainObjectReprRenderer usingLinkToBuilder(final ObjectAdapterLinkTo objectAdapterLinkToBuilder) {
        this.linkToBuilder = objectAdapterLinkToBuilder.usingUrlBase(rendererContext);
        return this;
    }

    @Override
    public DomainObjectReprRenderer with(final ObjectAdapter objectAdapter) {
        this.objectAdapter = objectAdapter;
        String domainTypeHref = DomainTypeReprRenderer.newLinkToBuilder(getRendererContext(), Rel.DOMAIN_TYPE, objectAdapter.getSpecification()).build().getString("href");
        addMediaTypeParams(X_RO_DOMAIN_TYPE, domainTypeHref);
        return this;
    }
    
    @Override
    public JsonRepresentation render() {

        final boolean isService = objectAdapter.getSpecification().isService();

        if (!(mode.isArgs())) {
            
            // self, extensions.oid
            if (objectAdapter.representsPersistent()) {
                if (includesSelf) {
                    addLinkToSelf();
                }
                getExtensions().mapPut("oid", getOidStr());
            }
            
            // title
            final String title = objectAdapter.titleString(null);
            representation.mapPut("title", title);
            
            // serviceId or instance Id
            if (isService) {
                representation.mapPut("serviceId", ServiceUtil.id(objectAdapter.getObject()));
            } else {
                final String domainType = getDomainType();
                final String instanceId = getInstanceId();
                if (domainType != null) {
                    representation.mapPut("domainType", domainType);
                    representation.mapPut("instanceId", instanceId);
                    
                }
            }
        }

        // members
        withMembers(objectAdapter);

        // described by
        if (mode.includeDescribedBy()) {
            addLinkToDescribedBy();
        }
        if(isService && mode.includeUp()) {
            addLinkToUp();
        }

        if (!mode.isArgs()) {
            // update/persist
            addPersistLinkIfTransientAndPersistable();
            addUpdatePropertiesLinkIfRequired();

            // extensions
            getExtensions().mapPut("isService", isService);
            getExtensions().mapPut("isPersistent", objectAdapter.representsPersistent());
        }

        return representation;
    }

    private void addLinkToSelf() {
        final JsonRepresentation link = linkToBuilder.with(objectAdapter).builder(Rel.SELF).build();

        final LinkFollowSpecs linkFollower = getLinkFollowSpecs().follow("links");
        if (linkFollower.matches(link)) {
            final DomainObjectReprRenderer renderer = new DomainObjectReprRenderer(getRendererContext(), linkFollower, JsonRepresentation.newMap());
            renderer.with(objectAdapter);
            link.mapPut("value", renderer.render());
        }

        getLinks().arrayAdd(link);
    }

    private void addLinkToDescribedBy() {
        final JsonRepresentation link = DomainTypeReprRenderer.newLinkToBuilder(getRendererContext(), Rel.DESCRIBEDBY, objectAdapter.getSpecification()).build();

        final LinkFollowSpecs linkFollower = getLinkFollowSpecs().follow("links");
        if (linkFollower.matches(link)) {
            final DomainTypeReprRenderer renderer = new DomainTypeReprRenderer(getRendererContext(), linkFollower, JsonRepresentation.newMap());
            renderer.with(objectAdapter.getSpecification());
            link.mapPut("value", renderer.render());
        }
        getLinks().arrayAdd(link);
    }

    private void addLinkToUp() {
        final JsonRepresentation link = LinkBuilder.newBuilder(rendererContext, Rel.UP.getName(), RepresentationType.LIST, "services").build();
        getLinks().arrayAdd(link);
    }

    private String getDomainType() {
        return org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils.getDomainType(objectAdapter);
    }

    private String getInstanceId() {
        return org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils.getInstanceId(rendererContext, objectAdapter);
    }

    private String getOidStr() {
        return org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils.getOidStr(rendererContext, objectAdapter);
    }

    private DomainObjectReprRenderer withMembers(final ObjectAdapter objectAdapter) {
        final JsonRepresentation appendTo = 
                mode.isUpdatePropertiesLinkArgs() ? representation : JsonRepresentation.newMap();
        final List<ObjectAssociation> associations = objectAdapter.getSpecification().getAssociations(Contributed.EXCLUDED);
        addAssociations(objectAdapter, appendTo, associations);

        if (mode.isRegular()) {
            final List<ObjectAction> actions = objectAdapter.getSpecification().getObjectActions(Contributed.INCLUDED);
            addActions(objectAdapter, actions, appendTo);
        }
        if(!mode.isUpdatePropertiesLinkArgs()) {
            representation.mapPut("members", appendTo);
        }
        return this;
    }

    private void addAssociations(final ObjectAdapter objectAdapter, final JsonRepresentation members, final List<ObjectAssociation> associations) {
        final LinkFollowSpecs linkFollower = getLinkFollowSpecs().follow("members");
        for (final ObjectAssociation assoc : associations) {

            if (mode.checkVisibility()) {
                final Consent visibility = assoc.isVisible(getRendererContext().getAuthenticationSession(), objectAdapter, rendererContext.getWhere());
                if (!visibility.isAllowed()) {
                    continue;
                }
            }
            if (assoc instanceof OneToOneAssociation) {
                final OneToOneAssociation property = (OneToOneAssociation) assoc;

                final ObjectPropertyReprRenderer renderer = new ObjectPropertyReprRenderer(getRendererContext(), linkFollower, property.getId(), JsonRepresentation.newMap());

                renderer.with(new ObjectAndProperty(objectAdapter, property)).usingLinkTo(linkToBuilder);

                if (mode.isArgs()) {
                    renderer.asArguments();
                }
                if(mode.isEventSerialization()) {
                    renderer.asEventSerialization();
                }

                members.mapPut(assoc.getId(), renderer.render());
            }

            if (mode.isArgs()) {
                continue;
            }
            if (assoc instanceof OneToManyAssociation) {
                final OneToManyAssociation collection = (OneToManyAssociation) assoc;

                final ObjectCollectionReprRenderer renderer = new ObjectCollectionReprRenderer(getRendererContext(), linkFollower, collection.getId(), JsonRepresentation.newMap());

                renderer.with(new ObjectAndCollection(objectAdapter, collection)).usingLinkTo(linkToBuilder);
                if(mode.isEventSerialization()) {
                    renderer.asEventSerialization();
                }

                members.mapPut(assoc.getId(), renderer.render());
            }
        }
    }

    private void addActions(final ObjectAdapter objectAdapter, final List<ObjectAction> actions, final JsonRepresentation members) {
        for (final ObjectAction action : actions) {
            final Consent visibility = action.isVisible(getRendererContext().getAuthenticationSession(), objectAdapter, rendererContext.getWhere());
            if (!visibility.isAllowed()) {
                continue;
            }
            final LinkFollowSpecs linkFollowSpecs = getLinkFollowSpecs().follow("members["+action.getId()+"]");
            
            final ObjectActionReprRenderer renderer = new ObjectActionReprRenderer(getRendererContext(), linkFollowSpecs, action.getId(), JsonRepresentation.newMap());

            renderer.with(new ObjectAndAction(objectAdapter, action)).usingLinkTo(linkToBuilder);

            members.mapPut(action.getId(), renderer.render());
        }
    }

    private void addPersistLinkIfTransientAndPersistable() {
        if (objectAdapter.representsPersistent()) {
            return;
        }
        if(objectAdapter.getSpecification().containsDoOpFacet(NotPersistableFacet.class)) {
            return;
        }
        final DomainObjectReprRenderer renderer = new DomainObjectReprRenderer(getRendererContext(), null, JsonRepresentation.newMap());
        final JsonRepresentation domainObjectRepr = renderer.with(objectAdapter).asPersistLinkArguments().render();

        final String domainType = objectAdapter.getSpecification().getSpecId().asString();
        final LinkBuilder persistLinkBuilder = LinkBuilder.newBuilder(getRendererContext(), Rel.PERSIST.getName(), RepresentationType.DOMAIN_OBJECT, "objects/%s", domainType).withHttpMethod(RestfulHttpMethod.POST).withArguments(domainObjectRepr);
        getLinks().arrayAdd(persistLinkBuilder.build());
    }

    private DomainObjectReprRenderer asPersistLinkArguments() {
        this.mode = Mode.PERSIST_LINK_ARGUMENTS;
        return this;
    }

    private DomainObjectReprRenderer asUpdatePropertiesLinkArguments() {
        this.mode = Mode.UPDATE_PROPERTIES_LINK_ARGUMENTS;
        return this;
    }

    // not part of the spec
    public DomainObjectReprRenderer asEventSerialization() {
        this.mode = Mode.EVENT_SERIALIZATION;
        return this;
    }


    private void addUpdatePropertiesLinkIfRequired() {
        if(mode.isEventSerialization()) {
            return;
        }
        if (!objectAdapter.representsPersistent()) {
            return;
        }
        final boolean isService = objectAdapter.getSpecification().isService();
        if(isService) {
            return;
        }

        final DomainObjectReprRenderer renderer = new DomainObjectReprRenderer(getRendererContext(), null, JsonRepresentation.newMap());
        final JsonRepresentation domainObjectRepr = renderer.with(objectAdapter).asUpdatePropertiesLinkArguments().render();

        final LinkBuilder updateLinkBuilder = LinkBuilder.newBuilder(getRendererContext(), Rel.UPDATE.getName(), RepresentationType.DOMAIN_OBJECT, "objects/%s/%s", getDomainType(), getInstanceId()).withHttpMethod(RestfulHttpMethod.PUT).withArguments(domainObjectRepr);
        getLinks().arrayAdd(updateLinkBuilder.build());
    }

    // ///////////////////////////////////////////////////////////////////
    //
    // ///////////////////////////////////////////////////////////////////

    public static Object valueOrRef(final RendererContext resourceContext, final ObjectAdapter objectAdapter, final ObjectSpecification objectSpec) {
        if(objectAdapter.isValue()) {
            String format = null; // TODO
            return JsonValueEncoder.asObject(objectAdapter, format);
        }
        final TitleFacet titleFacet = objectSpec.getFacet(TitleFacet.class);
        final String title = titleFacet.title(objectAdapter, resourceContext.getLocalization());
        return DomainObjectReprRenderer.newLinkToBuilder(resourceContext, Rel.VALUE, objectAdapter).withTitle(title).build();
    }

 
    
    // ///////////////////////////////////////////////////////////////////
    // dependencies (from context)
    // ///////////////////////////////////////////////////////////////////

    protected static OidMarshaller getOidMarshaller() {
		return IsisContext.getOidMarshaller();
	}

}